package com.example.redis_redisson_demo.config;


import org.redisson.Redisson;
import org.redisson.RedissonExecutorService;
import org.redisson.api.RedissonClient;
import org.redisson.api.RedissonReactiveClient;
import org.redisson.api.RedissonRxClient;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.config.Config;
import org.redisson.config.ReadMode;
import org.redisson.config.SubscriptionMode;
import org.redisson.connection.balancer.RoundRobinLoadBalancer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;
import java.util.concurrent.ExecutorService;

@Configuration
public class RedissonConfig {

    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
    /**
     * redisson通过直接配置RedissonConfig其实是可以做到对任何情况下(单节点/集群/哨兵/单主从)的链接选项进行配置
     * 真的在生产环境中不推荐从application.yml中获取下面的两个值，因为下面的两个值是专门给StringRedisTemplate在单节点模式下使用的
     * 推荐还是自己写一个properties来存放自定义yaml配置，就像RedissonProperties
     */
    @Value("${spring.redis.host}")
    private String redisHost;
    @Value("${spring.redis.port}")
    private String redisPort;
    @Autowired
    RedissonProperties.RedissonSingleProperties redissonSingleProperties;
    @Autowired
    RedissonProperties.RedissonClusterProperties redissonClusterProperties;
    @Autowired
    RedissonProperties.RedissonMasterSlaveProperties redissonMasterSlaveProperties;
    @Autowired
    RedissonProperties.RedissonSentinelProperties redissonSentinelProperties;

    /**
     * 配置RedissonClient的自动注入
     * 指定了一个销毁方法为shutdown
     *
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() throws IOException {
        // 获取Config
        Config redissonConfig = fixConfig();
        redissonConfig.useSingleServer().setClientName("RedissonClient");
        redissonConfig.useSingleServer().setConnectionMinimumIdleSize(2);
        // 创建RedissonClient
        RedissonClient redissonClient = Redisson.create(redissonConfig);
        return redissonClient;
    }

    /**
     * Redisson除了提供最常规也是最普遍使用的 RedissonClient之外，还提供另外两种Client
     * 首先是 RedissonReactiveClient，专门为反应式编程准备，构建方法和构建RedissonClient完全相同
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonReactiveClient redissonReactiveClient() throws IOException {
        // 获得Config
        Config redissonConfig = fixConfig();
        redissonConfig.useSingleServer().setClientName("RedissonReactiveClient");
        redissonConfig.useSingleServer().setConnectionMinimumIdleSize(2);
        // 创建 RedissonReactiveClient
        RedissonReactiveClient redissonReactiveClient = Redisson.createReactive(redissonConfig);
        return redissonReactiveClient;
    }

    /**
     * 其次是RedissonRxClient，是一个RxJava2规范的接口
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonRxClient redissonRxClient() throws IOException {
        Config redissonConfig = fixConfig();
        redissonConfig.useSingleServer().setClientName("RedissonRxClient");
        redissonConfig.useSingleServer().setConnectionMinimumIdleSize(2);
        RedissonRxClient redissonRxClient = Redisson.createRx(redissonConfig);
        return redissonRxClient;
    }


    /**
     * 单独抽取一个制作默认Config的方法
     *
     * @return
     */
    private Config fixConfig() {
        // 新建一个org.redisson.config.Config类
        Config redissonConfig = new Config();

        // TODO: Redisson通用设置

        /**
         * 很少用到的设置项:
         * eventLoopGroup
         * transportMode（传输模式）
         */

        /**
         * codec（编码）
         * 可选值见说明文档。默认值为 org.redisson.codec.JsonJacksonCodec
         */
        redissonConfig.setCodec(new JsonJacksonCodec());


        /**
         * threads（线程池数量）
         * 这个线程池数量被所有RTopic对象监听器、RRemoteService调用者和RExecutorService任务共同共享。
         * 默认值为 当前处理核数量 * 2
         */
        redissonConfig.setThreads(8 * 2);

        /**
         * nettyThreads （Netty线程池数量）
         * 这个线程池数量是在一个Redisson实例内，被其创建的所有分布式数据类型和服务，以及底层客户端所一同共享的线程池里保存的线程数量。
         * 默认值为 当前处理核数量 * 2
         */
        redissonConfig.setNettyThreads(8 * 2);

        /**
         * executor（线程池）
         * 单独提供一个用来执行所有RTopic对象监听器、RRemoteService调用者和RExecutorService任务的线程池（ExecutorService）实例。
         * 默认不启用。如启用，需提供一个 ExecutorService 的实现
         *
         * redissonConfig.setExecutor(new ExecutorService() {});
         */

        /**
         * lockWatchdogTimeout（监控锁的看门狗超时，单位：毫秒）
         * 监控锁的看门狗超时时间，单位为毫秒。
         * 该参数只适用于分布式锁的加锁请求中未明确使用leaseTimeout(锁失效时间)参数的情况。
         *
         * 可以简单理解为redisson为所有没有明确规定过期时间的分布式锁，自动添加一个过期时间。
         * 在一个RedissonClient实例被关闭前，看门狗每隔30000毫秒将锁的过期时间更新为30000毫秒之后，
         *   直到某一个时刻RedissonClient被关闭。
         *   此时如果锁被正常释放，那看门狗等于无效。若没有被正常释放，由于看门狗不再延长其有效时间，30000毫秒后这个锁必然能被释放(其实是删除)
         * 用来避免因客户端宕机而导致锁无法被释放的死锁情况。
         * 默认值30000。但是30秒太长了，足以对业务造成毁灭性打击，可是因为看门狗是对所有的锁生效，并不是对某个业务的锁单独生效，所以没法单独设置。
         * P.S. 看了源码之后知道，每个线程的锁都有自己的看门狗，以`LockWatchdogTimeout/3`间隔执行检查，若仍持有锁，则将其过期时间重新设置为`LockWatchdogTimeout`
         * P.S. 看了源码发现想要为每个业务自定义看门狗也很简单。想办法给类内的 internalLockLeaseTime 赋值即可。
         */
        redissonConfig.setLockWatchdogTimeout(30000);

        /**
         * keepPubSubOrder（保持订阅发布顺序）
         * 通过该参数来修改是否按订阅发布消息的接收顺序来处理消息，如果选否将对消息实施并行处理。
         * 该参数只适用于订阅发布消息的情况。
         * 默认值为true
         */
        redissonConfig.setKeepPubSubOrder(true);

        /**
         * performanceMode（高性能模式）
         * 用来指定高性能引擎的行为。由于该变量值的选用与使用场景息息相关（NORMAL除外）我们建议对每个参数值都进行尝试。
         * 该参数只在 Redisson PRO 版本中提供
         * 默认值为 HIGHER_THROUGHPUT，可选值见说明文档
         */


        // TODO:Redisson使用单redis结点配置
        if (redissonSingleProperties != null && !redissonSingleProperties.getNode().isEmpty()) {
            LOGGER.info("Redisson使用单点模式");
            /**
             * 以下为使用单redis结点模式时，RedissonConfig提供的参数
             * 单节点下，需要在 RedissonConfig.useSingleServer() 下进行进一步的set配置
             */

            /**
             * address（节点地址）
             * 指定单redis节点地址，通过 host:port 的格式来指定结点的地址
             * 允许使用redis://开头，此时将开启SSL链接
             */
            redissonConfig.useSingleServer().setAddress(redissonSingleProperties.getNode());

            /**
             * subscriptionConnectionMinimumIdleSize（发布和订阅连接的最小空闲连接数）
             * 用于发布和订阅连接的最小保持连接数（长连接）。
             * Redisson内部经常通过发布和订阅来实现许多功能。长期保持一定数量的发布订阅连接是必须的。
             * 该链接仅用于内部的发布和订阅功能
             * 默认值为 1
             */
            redissonConfig.useSingleServer().setSubscriptionConnectionMinimumIdleSize(1);

            /**
             * subscriptionConnectionPoolSize（发布和订阅连接池大小）
             * 用于发布和订阅连接的连接池最大容量。连接池的连接数量自动弹性伸缩。
             * 该链接仅用于内部的发布和订阅功能
             * 默认值为50
             */
            redissonConfig.useSingleServer().setSubscriptionConnectionPoolSize(50);

            /**
             * connectionMinimumIdleSize（最小空闲连接数）
             * 最小保持连接数（长连接）。长期保持一定数量的连接有利于提高瞬时写入反应速度
             * 无论链接是否空闲，redisson都不会释放/关闭这些链接
             * 默认值为32
             */
            redissonConfig.useSingleServer().setConnectionMinimumIdleSize(32);

            /**
             * connectionPoolSize（连接池大小）
             * 在启用该功能以后，Redisson将会监测DNS的变化情况。
             * 默认值为64
             */
            redissonConfig.useSingleServer().setConnectionPoolSize(64);

            /**
             * dnsMonitoringInterval（DNS监测时间间隔，单位：毫秒）
             * 监测DNS的变化情况的时间间隔。
             * 默认值为5000
             */
            redissonConfig.useSingleServer().setDnsMonitoringInterval(5000);

            /**
             * idleConnectionTimeout（连接空闲超时，单位：毫秒）
             * 如果当前连接池里的连接数量超过了最小空闲连接数，而同时有连接空闲时间超过了该数值，那么这些连接将会自动被关闭，并从连接池里去掉。时间单位是毫秒。
             * 默认值为10000
             */
            redissonConfig.useSingleServer().setIdleConnectionTimeout(10000);

            /**
             * connectTimeout（连接超时，单位：毫秒）
             * 同节点建立连接时的等待超时。时间单位是毫秒。
             * 默认值：10000
             */
            redissonConfig.useSingleServer().setConnectTimeout(10000);

            /**
             * timeout（命令等待超时，单位：毫秒）
             * 等待节点回复命令的时间。该时间从命令发送成功时开始计时。
             * 默认值：3000
             * 注意3秒对于高迸发系统已经是一个很大的值了
             */
            redissonConfig.useSingleServer().setTimeout(3000);

            /**
             * retryAttempts（命令失败重试次数）
             * 如果尝试达到 retryAttempts（命令失败重试次数） 仍然不能将命令发送至某个指定的节点时，将抛出错误。
             * 如果尝试在此限制之内发送成功，则开始启用 timeout（命令等待超时） 计时。
             * 默认值：3
             */
            redissonConfig.useSingleServer().setRetryAttempts(3);

            /**
             * retryInterval（命令重试发送时间间隔，单位：毫秒）
             * 如果不能将命令发送至某个指定的节点时，以 retryInterval 时间间隔尝试 retryAttempts次
             * 默认值：1500
             */
            redissonConfig.useSingleServer().setRetryInterval(1500);

            /**
             * 两个已经弃用的配置项:  failedAttempts（执行失败最大次数） / reconnectionTimeout（重新连接时间间隔）
             * 在某个节点执行相同或不同命令时，连续 失败 failedAttempts（执行失败最大次数） 时，该节点将被从可用节点列表里清除，直到 reconnectionTimeout（重新连接时间间隔） 超时以后再次尝试。
             */

            /**
             * database（数据库编号）
             * 尝试连接的数据库编号。
             * 默认值：0
             * 该值需要和application.yml相互配合。如果一个项目使用多数据库，则在application.yml中声明数据库地址时，最好同时赋予数据库编号
             */
            redissonConfig.useSingleServer().setDatabase(0);

            /**
             * password（密码）
             * 用于节点身份验证的密码。
             * 默认为null
             * redissonConfig.useSingleServer().setPassword(null);
             */

            /**
             * subscriptionsPerConnection（单个连接最大订阅数量）
             * 每个连接的最大订阅数量。
             * 默认值：5
             */
            redissonConfig.useSingleServer().setSubscriptionsPerConnection(5);

            /**
             * clientName（客户端名称）
             * 在Redis节点里显示的客户端名称。
             * 默认值为Null
             */
            redissonConfig.useSingleServer().setClientName("default");

            /**
             * 不常用配置:
             * sslEnableEndpointIdentification（启用SSL终端识别）
             * sslProvider（SSL实现方式）
             * sslTruststore（SSL信任证书库路径）
             * sslTruststorePassword（SSL信任证书库密码）
             * sslKeystore（SSL钥匙库路径）
             * sslKeystorePassword（SSL钥匙库密码）
             */
        }

        // TODO: Redisson使用redis集群时的配置
        /**
         * 集群模式下需要在 RedissonConfig.useClusterServers() 下进行进一步的set配置
         * Redisson使用redis集群的最底要求是必须有三个主节点
         */
        else if (redissonClusterProperties != null && !redissonClusterProperties.getNodes().isEmpty()) {
            LOGGER.info("Redisson使用集群模式");
            /**
             * nodeAddresses（添加节点地址）
             * 可以通过host:port的格式来添加Redis集群节点的地址。多个节点可以一次性批量添加，也可以使用多次nodeAddress
             * 主节点从节点均可以使用该命令添加
             */
//            redissonConfig.useClusterServers().addNodeAddress(
//                    "redis://127.0.0.1:6379",
//                    "redis://127.0.0.1:6380",
//                    "redis://127.0.0.1:6381");
            for (String node : redissonClusterProperties.getNodes()) {
                redissonConfig.useClusterServers().addNodeAddress(node);
            }

            /**
             * scanInterval（集群扫描间隔时间）
             * 对Redis集群节点状态扫描的时间间隔。单位是毫秒。
             * 默认值： 1000
             */
            redissonConfig.useClusterServers().setScanInterval(1000);

            /**
             * slots（分片数量）
             * 默认值： 231 用于指定数据分片过程中的分片数量。
             * 支持数据分片/框架结构有：集（Set）、映射（Map）、BitSet、Bloom filter, Spring Cache和Hibernate Cache等.
             */

            /**
             * readMode（读取操作的负载均衡模式）
             * 设置读取操作选择节点的模式。
             * 可用值为：SLAVE - 只在从服务节点里读取。 MASTER - 只在主服务节点里读取。 MASTER_SLAVE - 在主从服务节点里都可以读取。
             * 默认值： ReadMode.SLAVE
             */
            redissonConfig.useClusterServers().setReadMode(ReadMode.SLAVE);

            /**
             * subscriptionMode（订阅操作的负载均衡模式）
             * 设置订阅操作选择节点的模式。
             * 可用值为： SLAVE - 只在从服务节点里订阅。 MASTER - 只在主服务节点里订阅。
             * 默认值：SubscriptionMode.SLAVE
             */
            redissonConfig.useClusterServers().setSubscriptionMode(SubscriptionMode.SLAVE);

            /**
             * loadBalancer（负载均衡算法类的选择）
             * 在多Redis服务节点的环境里，可以选用以下几种负载均衡方式选择一个节点：
             *   org.redisson.connection.balancer.WeightedRoundRobinBalancer - 权重轮询调度算法
             *   org.redisson.connection.balancer.RoundRobinLoadBalancer - 轮询调度算法
             *   org.redisson.connection.balancer.RandomLoadBalancer - 随机调度算法
             * 默认值为RoundRobinLoadBalancer
             */
            redissonConfig.useClusterServers().setLoadBalancer(new RoundRobinLoadBalancer());

            /**
             * subscriptionConnectionMinimumIdleSize（从节点发布和订阅连接的最小空闲连接数）
             * 多从节点的环境里，每个 从服务节点里用于发布和订阅连接的最小保持连接数（长连接）。
             * Redisson内部经常通过发布和订阅来实现许多功能。长期保持一定数量的发布订阅连接是必须的。
             * 默认值：1
             */
            redissonConfig.useClusterServers().setSubscriptionConnectionMinimumIdleSize(1);

            /**
             * subscriptionConnectionPoolSize（从节点发布和订阅连接池大小）
             * 多从节点的环境里，每个 从服务节点里用于发布和订阅连接的连接池最大容量。连接池的连接数量自动弹性伸缩。
             * 默认值：50
             */
            redissonConfig.useClusterServers().setSubscriptionConnectionPoolSize(50);

            /**
             * 以下内容不再赘述
             * slaveConnectionMinimumIdleSize（从节点最小空闲连接数）
             * slaveConnectionPoolSize（从节点连接池大小）
             * masterConnectionMinimumIdleSize（主节点最小空闲连接数）
             * masterConnectionPoolSize（主节点连接池大小）
             * idleConnectionTimeout（连接空闲超时，单位：毫秒）
             * connectTimeout（连接超时，单位：毫秒）
             * timeout（命令等待超时，单位：毫秒）
             * retryAttempts（命令失败重试次数）
             * retryInterval（命令重试发送时间间隔，单位：毫秒）
             * password（密码）
             * subscriptionsPerConnection（单个连接最大订阅数量）
             * clientName（客户端名称）
             * SSL(SSL链接)
             */
        }


        // TODO：Redisson使用哨兵模式时的配置
        /**
         * 哨兵模式下需要在 RedissonConfig.useSentinelServers()下进行进一步的set配置
         * 哨兵模式要求仅有一个主节点，从节点个数任意。sentinel结点任意但有推荐值
         */
        else if (redissonSentinelProperties != null && !redissonSentinelProperties.getNodes().isEmpty()) {
            LOGGER.info("Redisson使用哨兵模式");
            /**
             * setMasterName 设置监视的主节点别名
             */
            redissonConfig.useSentinelServers()
                    // 设置sentinel监视的主节点的名字。注意这里不是随便取的，是在redis上配置sentinel时用的啥这里就得写啥
                    .setMasterName("mymaster");

            /**
             * addSentinelAddress 添加sentinel结点地址
             */
//            redissonConfig.useSentinelServers()
//                    // 添加sentinel结点，将监视上面这个主节点的所有sentinel都添加进来
//                    .addSentinelAddress("127.0.0.1:26389")
//                    .addSentinelAddress("127.0.0.1:26390")
//                    .addSentinelAddress("127.0.0.1:26391");
            for (String node : redissonSentinelProperties.getNodes()) {
                redissonConfig.useSentinelServers().addSentinelAddress(node);
            }

            /**
             * 设置读取操作选择节点的模式。 可用值为：
             * SLAVE - 只在从服务节点里读取。 MASTER - 只在主服务节点里读取。 MASTER_SLAVE - 在主从服务节点里都可以读取。
             */
            redissonConfig.useSentinelServers().setReadMode(ReadMode.SLAVE);
        }


        // TODO: Redisson使用redis主从模式时的配置
        /**
         * 集群模式下需要在 RedissonConfig.useMasterSlaveServers() 下进行进一步的set配置
         * 要求仅有一个主节点，从节点数量不限
         */
        else if (redissonMasterSlaveProperties != null && !redissonMasterSlaveProperties.getMaster().isEmpty()) {
            LOGGER.info("Redisson使用主从模式");
            /**
             * masterAddress（主节点地址）
             */
            redissonConfig.useMasterSlaveServers().setMasterAddress(redissonMasterSlaveProperties.getMaster());

            /**
             * addSlaveAddress（添加从主节点地址）
             */
            for (String slave : redissonMasterSlaveProperties.getSlave()) {
                redissonConfig.useMasterSlaveServers().addSlaveAddress(slave);
            }

            /**
             * 其他参数和集群模式相同，不再赘述
             */
        }


        return redissonConfig;
    }

}
